Skip to content

Auto-resume agent sessions on app restore#3025

Closed
yourconscience wants to merge 3 commits intomanaflow-ai:mainfrom
yourconscience:feat/agent-session-restore
Closed

Auto-resume agent sessions on app restore#3025
yourconscience wants to merge 3 commits intomanaflow-ai:mainfrom
yourconscience:feat/agent-session-restore

Conversation

@yourconscience
Copy link
Copy Markdown

@yourconscience yourconscience commented Apr 20, 2026

Persists resume commands for Claude Code, Codex, and OpenCode in the session snapshot. On restore, agent sessions automatically resume in their original panes via initialInput.

Resume commands are cached when agent PIDs are registered (socket API), not at snapshot time, so the autosave hot path stays zero-cost. Remote-backed panels are excluded. Cache entries are pruned when panels close.

Test plan

  • Run Claude Code in a pane, quit cmux, relaunch - session resumes
  • Same for Codex and OpenCode
  • Remote-backed panels don't get agent resume commands

Summary by CodeRabbit

  • New Features
    • Sessions now automatically capture and restore agent command history, ensuring previously executed commands are replayed when sessions are restored.
    • Session persistence improved to include agent state preservation, maintaining workflow continuity across restarts and reducing friction in agent-assisted development.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 20, 2026

@yourconscience is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

This change implements agent resume command tracking and restoration across the session management system. It adds a new API to fetch recent session entries, extends session persistence with resume command storage, integrates resolution calls in terminal control, and implements comprehensive caching and restoration logic in Workspace.

Changes

Cohort / File(s) Summary
Session Index API
Sources/SessionIndexStore.swift
Added latestEntries(agent:cwd:limit:) nonisolated async static method to fetch recent SessionEntry items per agent filtered by working directory, delegating to per-agent loaders with ErrorBag handling for Codex and OpenCode.
Session Persistence
Sources/SessionPersistence.swift
Extended SessionTerminalPanelSnapshot with optional agentResumeCommand: String? field to persist resume commands during snapshot capture.
Terminal Command Integration
Sources/TerminalController.swift
Integrated three calls to tab.resolveAndCacheResumeCommand(agentKey:pid:) in socket command handlers for sidebar metadata upsert and agent PID assignment operations.
Workspace Cache & Restoration
Sources/Workspace.swift
Implemented agent resume command caching infrastructure: added cachedAgentResumeCommands cache, helper methods for session agent lookup and TTY resolution via sysctl, async resolution/caching logic, and session restoration flow to replay cached commands as initial terminal input.

Sequence Diagram

sequenceDiagram
    participant TC as TerminalController
    participant WS as Workspace
    participant SIS as SessionIndexStore
    participant SP as SessionPersistence
    
    rect rgba(100, 150, 200, 0.5)
    Note over TC,SP: Live Session - Resolve & Cache Resume Command
    TC->>WS: resolveAndCacheResumeCommand(agentKey, pid)
    WS->>WS: ttyForPID(pid) via sysctl
    WS->>SIS: latestEntries(agent, cwd, limit)
    SIS-->>WS: [SessionEntry with resumeCommand]
    WS->>WS: Cache resumeCommand in cachedAgentResumeCommands[panelUUID]
    end
    
    rect rgba(150, 100, 200, 0.5)
    Note over TC,SP: Session Restoration - Replay Resume Commands
    TC->>WS: Session restore begins
    WS->>SP: Snapshot includes agentResumeCommand
    SP-->>WS: SessionTerminalPanelSnapshot
    WS->>WS: Compute initialInput from snapshot.agentResumeCommand
    WS->>TC: newTerminalSurface with initialInput
    TC->>TC: Terminal created with resumed command
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Add Sessions panel to right sidebar #2936: Introduces SessionIndexStore with per-agent loaders; this PR extends it by adding the latestEntries(agent:cwd:limit:) API that delegates to those loaders for resume command fetching.

Poem

🐰 Hop, cache, and restore with grace,
Resume commands find their place,
From index store to TTY track,
Agent sessions welcome you back!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete. It covers the summary and test plan sections from the template but lacks the Testing subsections, Demo Video, Review Trigger, and Checklist required by the repository template. Add detailed testing methodology, include a demo video for the UI behavior change, add the Review Trigger comment block, and complete the checklist items to fully comply with the repository's PR description template.
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature: auto-resuming agent sessions on app restore, which matches the primary change across the file modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
Sources/SessionIndexStore.swift (1)

525-528: Minor: unused ErrorBag on the Claude branch.

loadClaudeEntries does not accept an errorBag, so errors on the Claude path are silently swallowed here (consistent with scanAll, but worth a note). If future work adds error reporting to the Claude loader, remember to thread bag through; otherwise the let bag = ErrorBag() allocation is unused for .claude. Not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SessionIndexStore.swift` around lines 525 - 528, The code creates an
unused ErrorBag instance (ErrorBag) before switching on agent and immediately
returns from the .claude branch by calling
loadClaudeEntries(needle:cwdFilter:offset:limit:), so the ErrorBag allocation is
redundant and any errors on the Claude path aren’t captured; either remove the
unused let bag = ErrorBag() or, if you intend to support error reporting for
Claude in future, modify loadClaudeEntries to accept an ErrorBag parameter and
thread bag through (also mirror how scanAll uses ErrorBag) so errors are
reported rather than silently dropped.
Sources/ContentView.swift (1)

2841-2850: Consider falling back to any available panel when focusedPanelId is nil.

When no panel is focused (e.g., the user opened the Files panel before interacting with any terminal/editor), the markdown route is skipped entirely and the file opens in an external editor, silently ignoring the openMarkdownInCmuxViewer preference. A similar pattern exists elsewhere in Workspace (e.g., openEditor(filePath:) falls back to bonsplitController.allPaneIds.first when focusedPaneId is nil) to avoid dropping requests.

If Workspace exposes an analogous first-pane/first-panel fallback, preferring it over the unconditional external-editor path would better honor the user's setting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 2841 - 2850, The current
onOpenMarkdown closure returns early to external editor when
workspace.focusedPanelId is nil; change it to prefer any available panel by
obtaining a fallback panel id (e.g., let panelId = workspace.focusedPanelId ??
workspace.bonsplitController.allPaneIds.first or a Workspace-provided
firstPanelId) and pass that to workspace.openOrFocusMarkdownSplit(from: panelId,
filePath: path); only call PreferredEditorSettings.open(URL(fileURLWithPath:
path)) if openOrFocusMarkdownSplit returns nil. Update the code paths
referencing tabManager.selectedWorkspace, workspace.focusedPanelId,
workspace.openOrFocusMarkdownSplit(...), and PreferredEditorSettings.open(...)
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/SessionIndexStore.swift`:
- Around line 516-534: Add a new SessionEntry instance method
resumeCommandWithCwd() that returns the guarded resume command by shell-quoting
the SessionEntry.cwd and returning "cd <quoted cwd> && <resumeCommand>" (or just
the resumeCommand if cwd is empty); then update all resume-command call sites to
use it instead of the bare entry.resumeCommand—specifically change
resolveAndCacheResumeCommand(...) (where the value is cached for initialInput),
ContentView.resumeSession, the Workspace drag-drop terminal restore, and
SessionIndexView clipboard copy to call entry.resumeCommandWithCwd(); ensure the
shell-quoting used is the same utility/convention used elsewhere in the codebase
to prevent rc-file bypass.

In `@Sources/SessionPersistence.swift`:
- Around line 227-230: Add an isRemoteBacked Bool to
SessionTerminalPanelSnapshot and use it to prevent applying agentResumeCommand
to remote-backed panels: when restoring in the Workspace restore path (the code
that reads SessionTerminalPanelSnapshot and constructs resumeInput and calls
newTerminalSurface(... initialInput: ...)), only set resumeInput and pass
agentResumeCommand as initialInput if snapshot.isRemoteBacked is false (mirror
the existing gating used for initialInput and panelRestoreCommands). Ensure the
new isRemoteBacked field is populated in snapshots and used in the conditional
around the resumeInput assignment and the initialInput argument to
newTerminalSurface so remote-backed panels never receive detected or restore
commands.

In `@Sources/Workspace.swift`:
- Around line 472-475: The SessionTerminalPanelSnapshot is currently given
agentResumeCommand(forPanelId:) unconditionally, which allows persisted
initialInput to be replayed into remote-backed terminals; update the snapshot
construction (where SessionTerminalPanelSnapshot is created) to check the
panel's per-panel remote-backed flag (the same flag used by
createPanel(from:inPane:)) and only pass agentResumeCommand (initialInput) when
the panel is local (not remote-backed); apply the same guard to the other
occurrence around lines 707-712 so remote-backed panels never receive
initialInput from agentResumeCommand(forPanelId:).
- Around line 599-602: The code caches entry.resumeCommand directly, but you
must cache the cwd-guarded helper version so restored terminals start in the
intended directory; replace storing entry.resumeCommand into
cachedAgentResumeCommands[panelId] with the SessionEntry-provided cwd-guarded
resume command (use the helper on the SessionEntry instance, passing panelCwd so
it emits something like "cd <shell-quoted cwd> && <resumeCommand>"), i.e. call
the SessionEntry helper that builds the cwd-guarded resume command and store
that result instead of entry.resumeCommand.
- Around line 550-556: The stored property cachedAgentResumeCommands is declared
inside an extension and must be moved into the Workspace class body near
agentPIDs and restoredTerminalScrollbackByPanelId; remove the stored var from
the extension and keep only helper methods there. When populating the cache
(where entry.resumeCommand is used), store a cwd-guarded resume command (e.g.,
combine/validate the resume command with the panel's cwd or wrap with a guard
that checks cwd on replay) instead of the raw entry.resumeCommand. When
replaying cached commands (the code around SessionTerminalPanelSnapshot
handling), only replay for local terminals by gating on the per-panel
isRemoteBacked flag (skip replay if isRemoteBacked is true). Update references
to cachedAgentResumeCommands, the population site, and the replay site
accordingly so the extension methods access the moved property on Workspace.

---

Nitpick comments:
In `@Sources/ContentView.swift`:
- Around line 2841-2850: The current onOpenMarkdown closure returns early to
external editor when workspace.focusedPanelId is nil; change it to prefer any
available panel by obtaining a fallback panel id (e.g., let panelId =
workspace.focusedPanelId ?? workspace.bonsplitController.allPaneIds.first or a
Workspace-provided firstPanelId) and pass that to
workspace.openOrFocusMarkdownSplit(from: panelId, filePath: path); only call
PreferredEditorSettings.open(URL(fileURLWithPath: path)) if
openOrFocusMarkdownSplit returns nil. Update the code paths referencing
tabManager.selectedWorkspace, workspace.focusedPanelId,
workspace.openOrFocusMarkdownSplit(...), and PreferredEditorSettings.open(...)
accordingly.

In `@Sources/SessionIndexStore.swift`:
- Around line 525-528: The code creates an unused ErrorBag instance (ErrorBag)
before switching on agent and immediately returns from the .claude branch by
calling loadClaudeEntries(needle:cwdFilter:offset:limit:), so the ErrorBag
allocation is redundant and any errors on the Claude path aren’t captured;
either remove the unused let bag = ErrorBag() or, if you intend to support error
reporting for Claude in future, modify loadClaudeEntries to accept an ErrorBag
parameter and thread bag through (also mirror how scanAll uses ErrorBag) so
errors are reported rather than silently dropped.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2e2cceae-1dea-47f8-8c23-35835ce0854f

📥 Commits

Reviewing files that changed from the base of the PR and between e976893 and 5327fc3.

📒 Files selected for processing (7)
  • Sources/ContentView.swift
  • Sources/FileExplorerView.swift
  • Sources/RightSidebarPanelView.swift
  • Sources/SessionIndexStore.swift
  • Sources/SessionPersistence.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift

Comment thread Sources/SessionIndexStore.swift
Comment thread Sources/SessionPersistence.swift
Comment thread Sources/Workspace.swift Outdated
Comment thread Sources/Workspace.swift Outdated
Comment thread Sources/Workspace.swift
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR adds two independent features: auto-resuming Claude Code / Codex / OpenCode sessions on app restore by persisting a synthesized resume command in the session snapshot, and routing the Files-panel "Open in Default Editor" action through the existing preferredEditor / openMarkdownInCmuxViewer settings.

  • P1 — completed sessions spuriously resume: clearAgentPID removes the PID from agentPIDs but does not evict cachedAgentResumeCommands. Any snapshot taken after an agent finishes cleanly (but before the app quits) will still carry the resume command, causing the terminal to re-run it on next launch.
  • P2 — session IDs unquoted in resume commands: Flag values go through shellQuote but the positional session ID does not; Codex and OpenCode IDs come from SQLite and are not format-constrained.

Confidence Score: 4/5

Safe to merge after fixing the stale-resume-command bug; the Files-panel routing change is clean.

One concrete P1 defect: clearAgentPID does not clear cachedAgentResumeCommands, so a session that finishes before the app quits will be spuriously resumed on next launch. All other findings are P2 or lower. The Files-panel editor routing and the new snapshot field are both correct.

Sources/Workspace.swift — clearAgentPID path and resolveAndCacheResumeCommand

Important Files Changed

Filename Overview
Sources/Workspace.swift Adds TTY-based PID→panel matching and resume-command caching; clearAgentPID does not evict the cached command, causing spurious resume on app restore after a session completes normally.
Sources/SessionIndexStore.swift Adds latestEntries(agent:cwd:limit:) static helper used to look up the most-recent session at PID-registration time; session IDs are not shell-quoted in resumeCommand.
Sources/SessionPersistence.swift Adds optional agentResumeCommand field to SessionTerminalPanelSnapshot; Codable/Sendable conformance is clean and backward-compatible.
Sources/FileExplorerView.swift Routes "Open in Default Editor" through CmdClickMarkdownRouteSettings and PreferredEditorSettings, consistent with how cmd-click links are handled; fallback to NSWorkspace is preserved.
Sources/RightSidebarPanelView.swift Adds optional onOpenMarkdown callback prop and threads it down to FileExplorerPanelView; clean plumbing change.
Sources/ContentView.swift Wires onOpenMarkdown to workspace.openOrFocusMarkdownSplit, with correct fallback to PreferredEditorSettings.open when no focused panel is available.

Sequence Diagram

sequenceDiagram
    participant Shell as Shell / Agent
    participant TC as TerminalController
    participant WS as Workspace
    participant SIS as SessionIndexStore
    participant Snap as SessionPersistence

    Shell->>TC: report_tty ttys001
    TC->>WS: surfaceTTYNames[panelId] = ttys001

    Shell->>TC: set_agent_pid claude_code 12345
    TC->>WS: resolveAndCacheResumeCommand(claude_code, 12345)
    WS->>WS: ttyForPID(12345) returns ttys001
    WS->>WS: match panelId via surfaceTTYNames
    WS->>SIS: latestEntries(agent:.claude, cwd:/proj)
    SIS-->>WS: SessionEntry with resumeCommand
    WS->>WS: cachedAgentResumeCommands[panelId] = cmd

    Note over TC,WS: Agent session finishes
    Shell->>TC: clear_agent_pid claude_code
    TC->>WS: agentPIDs.removeValue(claude_code)
    Note over WS: cachedAgentResumeCommands NOT cleared

    WS->>Snap: snapshotPanel includes agentResumeCommand

    Note over Snap: App quits and relaunches
    Snap->>WS: restorePanel with agentResumeCommand
    WS->>Shell: initialInput = resume cmd + newline, spurious resume
Loading

Comments Outside Diff (2)

  1. Sources/SessionIndexStore.swift, line 64-99 (link)

    P2 Session IDs not shell-quoted in resume commands

    The positional session ID argument is interpolated directly into the command string without going through shellQuote, unlike the flag values (model, permissionMode, etc.) which are quoted. For Claude the session ID is always a UUID-like filename (safe), but for Codex (SQL id field) and OpenCode (SQLite session.id) the ID format is not guaranteed — a specially crafted value in either database could cause unexpected shell interpretation when the command is auto-executed via initialInput.

    // Example: safe to quote the session ID the same way as flags
    var parts = ["claude --resume \(Self.shellQuote(sessionId))"]
    // and for the others:
    var parts = ["codex resume \(Self.shellQuote(sessionId))"]
    var parts = ["opencode --session \(Self.shellQuote(sessionId))"]
  2. Sources/Workspace.swift, line 759-773 (link)

    P1 Completed-session resume command not cleared on clear_agent_pid

    When an agent session finishes it calls clear_agent_pid, which removes the entry from agentPIDs but leaves cachedAgentResumeCommands[panelId] untouched. The next autosave (every 8 s) will snapshot the stale resume command, and on the next app restore the terminal will auto-execute the finished session's resume command as initialInput — even though the session already completed normally.

    The cache is only purged in applySnapshot (on restore) and resetSidebarContext, not on the happy-path completion signal. The fix is to also evict the matching cachedAgentResumeCommands entry when a PID is cleared, by re-resolving the TTY for the exiting PID before removing it from agentPIDs.

Reviews (1): Last reviewed commit: "Route Files panel opens through preferre..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/SessionIndexStore.swift">

<violation number="1" location="Sources/SessionIndexStore.swift:532">
P2: `latestEntries` performs synchronous OpenCode DB/file I/O in an async method, so it can block the caller executor (including main actor).</violation>
</file>

<file name="Sources/Workspace.swift">

<violation number="1" location="Sources/Workspace.swift:556">
P2: New per-panel resume-command cache is not pruned with other panel metadata, allowing stale UUID entries to accumulate during normal panel churn.</violation>

<violation number="2" location="Sources/Workspace.swift:595">
P2: CWD-based resume command lookup runs in remote workspaces, which can map local indexed sessions into remote terminals.</violation>

<violation number="3" location="Sources/Workspace.swift:707">
P1: Agent resume command is replayed unconditionally — remote-backed panels will receive a local `claude --resume …` (or similar) as `initialInput`, injecting it into the remote bootstrap shell. Gate `resumeInput` on the panel not being remote-backed, consistent with other restore paths that skip `initialInput` for remote panels.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/Workspace.swift
Comment thread Sources/SessionIndexStore.swift
Comment thread Sources/Workspace.swift Outdated
Comment thread Sources/Workspace.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
Sources/Workspace.swift (2)

582-599: ⚠️ Potential issue | 🟠 Major

Gate by the matched panel and cache the cwd-guarded command.

guard !isRemoteWorkspace skips local terminals that happen to live in a remote workspace; the remote check should be per matched panel. Also, this still stores raw entry.resumeCommand, so restored fresh shells can resume from the wrong cwd.

🔧 Proposed fix
     func resolveAndCacheResumeCommand(agentKey: String, pid: pid_t) {
-        guard !isRemoteWorkspace else { return }
         guard let agent = Self.sessionAgentForKey(agentKey) else { return }
         guard pid > 0 else { return }
 
         // Match PID to panel via TTY
         guard let pidTTY = Self.ttyForPID(pid) else { return }
         let matchedPanelId: UUID? = surfaceTTYNames.first(where: { $0.value == pidTTY })?.key
         guard let panelId = matchedPanelId else { return }
+        guard !remoteDetectedSurfaceIds.contains(panelId),
+              !isRemoteTerminalSurface(panelId) else {
+            cachedAgentResumeCommands.removeValue(forKey: panelId)
+            return
+        }
         let panelCwd = panelDirectories[panelId] ?? currentDirectory
         guard !panelCwd.isEmpty else { return }
 
         Task {
             let entries = await SessionIndexStore.latestEntries(agent: agent, cwd: panelCwd, limit: 1)
             guard let entry = entries.first else { return }
+            let resumeCommand = entry.resumeCommandWithCwd
             await MainActor.run {
-                self.cachedAgentResumeCommands[panelId] = entry.resumeCommand
+                guard self.panels[panelId] != nil,
+                      !(self.remoteDetectedSurfaceIds.contains(panelId) || self.isRemoteTerminalSurface(panelId)) else {
+                    return
+                }
+                self.cachedAgentResumeCommands[panelId] = resumeCommand
             }
         }
     }

Based on learnings: “always build resume commands using the cwd guard helper exposed by SessionEntry … cd <shell-quoted cwd> && <resumeCommand>” and “remote-backed terminals … omit restore/detected commands.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 582 - 599, The function
resolveAndCacheResumeCommand currently gates using the global isRemoteWorkspace
and stores raw entry.resumeCommand; change it to determine remote status per
matched panel (use matchedPanelId and panelDirectories) and skip caching if that
panel is remote-backed, then build and cache a guarded resume command using the
SessionEntry helper (i.e., prepend a cwd-guarding cd with proper shell-quoting
of panelCwd so the stored command is "cd <shell-quoted cwd> &&
<entry.resumeCommand>") instead of storing raw resumeCommand; use
Self.sessionAgentForKey, Self.ttyForPID, surfaceTTYNames,
SessionIndexStore.latestEntries, SessionEntry.resumeCommand and
cachedAgentResumeCommands to locate and update the code paths.

703-708: ⚠️ Potential issue | 🟠 Major

Keep the restore-time remote gate on initialInput.

This replays agentResumeCommand directly from the snapshot. Please also gate replay with the per-panel remote-backed snapshot flag, so an older or malformed snapshot cannot inject a local resume command into a remote startup shell.

🛡️ Proposed fix
-            let resumeInput = snapshot.terminal?.agentResumeCommand.map { $0 + "\n" }
+            let panelWasRemoteBacked = snapshot.terminal?.isRemoteBacked == true
+            let resumeInput = panelWasRemoteBacked
+                ? nil
+                : snapshot.terminal?.agentResumeCommand.map { $0 + "\n" }
             guard let terminalPanel = newTerminalSurface(
                 inPane: paneId,
                 focus: false,
                 workingDirectory: workingDirectory,
                 initialInput: resumeInput,

Based on learnings: “createPanel(from:inPane:) uses this per-panel flag … pass initialInput only for local panels.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 703 - 708, The code currently replays
snapshot.terminal?.agentResumeCommand into newTerminalSurface via initialInput
unconditionally; change it to only set initialInput when the panel is not
remote-backed (i.e., gate replay with the per-panel remote-backed snapshot flag
used by createPanel(from:inPane:)). Concretely, compute resumeInput only if the
snapshot/panel indicates a local panel (negate the per-panel remote-backed flag)
and pass nil for initialInput for remote-backed panels so
newTerminalSurface(inPane:focus:workingDirectory:initialInput:) never injects a
local resume command into a remote startup shell.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/Workspace.swift`:
- Around line 582-599: The function resolveAndCacheResumeCommand currently gates
using the global isRemoteWorkspace and stores raw entry.resumeCommand; change it
to determine remote status per matched panel (use matchedPanelId and
panelDirectories) and skip caching if that panel is remote-backed, then build
and cache a guarded resume command using the SessionEntry helper (i.e., prepend
a cwd-guarding cd with proper shell-quoting of panelCwd so the stored command is
"cd <shell-quoted cwd> && <entry.resumeCommand>") instead of storing raw
resumeCommand; use Self.sessionAgentForKey, Self.ttyForPID, surfaceTTYNames,
SessionIndexStore.latestEntries, SessionEntry.resumeCommand and
cachedAgentResumeCommands to locate and update the code paths.
- Around line 703-708: The code currently replays
snapshot.terminal?.agentResumeCommand into newTerminalSurface via initialInput
unconditionally; change it to only set initialInput when the panel is not
remote-backed (i.e., gate replay with the per-panel remote-backed snapshot flag
used by createPanel(from:inPane:)). Concretely, compute resumeInput only if the
snapshot/panel indicates a local panel (negate the per-panel remote-backed flag)
and pass nil for initialInput for remote-backed panels so
newTerminalSurface(inPane:focus:workingDirectory:initialInput:) never injects a
local resume command into a remote startup shell.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2abcfc3a-231a-43e8-ba02-d81c31c0feb8

📥 Commits

Reviewing files that changed from the base of the PR and between 5327fc3 and 9e0a4be.

📒 Files selected for processing (1)
  • Sources/Workspace.swift

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/Workspace.swift">

<violation number="1" location="Sources/Workspace.swift:11972">
P2: Detaching/moving a panel clears `cachedAgentResumeCommands` without transferring it, so moved live agent terminals can lose persisted auto-resume command data.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/Workspace.swift
@yourconscience yourconscience changed the title Auto-resume agent sessions and route Files panel through editor settings Auto-resume agent sessions + Files panel editor routing Apr 20, 2026
When cmux quits and relaunches, terminal panels that were running an agent
(Claude Code, Codex, OpenCode) now automatically resume the session.

At snapshot time, agent PIDs (already tracked via the socket API) are matched
to terminal panels by comparing the PID's controlling TTY to each panel's
surfaceTTYName. When matched, the most recent session file for that agent+cwd
is looked up from disk and its resume command is stored in the session snapshot.

On restore, the saved resume command is sent as initialInput to the new
terminal, so the agent picks up where it left off.

Supports:
- Claude Code: scans ~/.claude/projects/*/
- Codex: scans ~/.codex/sessions/YYYY/MM/DD/
- OpenCode: queries ~/.local/share/opencode/opencode.db

No agent CLI changes required. The agentResumeCommand field is optional in
the snapshot schema, so existing sessions restore normally without it.
When cmux quits and relaunches, terminal panels that were running an agent
(Claude Code, Codex, OpenCode) now automatically resume the session.

Design: resume commands are cached eagerly when agent PIDs are registered
via the socket API (set_agent_pid / report_status), not during the autosave
snapshot. This keeps the 8s autosave hot path free of disk I/O.

Flow:
1. Agent CLI reports its PID via socket -> TerminalController
2. Workspace.resolveAndCacheResumeCommand matches PID to panel via sysctl
   TTY, then looks up the latest session via SessionIndexStore.latestEntries
3. The resume command (including per-session flags like model/permission
   mode) is cached in cachedAgentResumeCommands[panelId]
4. On autosave, sessionPanelSnapshot reads the cached command (zero I/O)
5. On restore, the command is sent as initialInput to the new terminal

No agent CLI changes required. No new socket API. Reuses existing
SessionIndexStore loaders for correct Codex SQL, OpenCode schema, and
Claude project directory resolution.
@yourconscience yourconscience force-pushed the feat/agent-session-restore branch from 9e0a4be to 78fea9d Compare April 20, 2026 10:50
@yourconscience yourconscience changed the title Auto-resume agent sessions + Files panel editor routing Auto-resume agent sessions on app restore Apr 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
Sources/SessionIndexStore.swift (1)

522-534: Silently discarded errors from Codex/OpenCode loaders.

bag is populated inside loadCodexEntries/loadOpenCodeEntries on schema/open failures but is then dropped when this method returns. For the resume-caching call site that's probably fine (best-effort), but it means a broken Codex DB schema will silently yield [] and the agent will never auto-resume, with no diagnostic. Consider at least a dlog of bag.snapshot() under #if DEBUG so this failure mode is diagnosable.

🔎 Suggested diff
     nonisolated static func latestEntries(
         agent: SessionAgent, cwd: String, limit: Int = 1
     ) async -> [SessionEntry] {
         let bag = ErrorBag()
+        let results: [SessionEntry]
         switch agent {
         case .claude:
-            return await loadClaudeEntries(needle: "", cwdFilter: cwd, offset: 0, limit: limit)
+            results = await loadClaudeEntries(needle: "", cwdFilter: cwd, offset: 0, limit: limit)
         case .codex:
-            return await loadCodexEntries(needle: "", cwdFilter: cwd, offset: 0, limit: limit, errorBag: bag)
+            results = await loadCodexEntries(needle: "", cwdFilter: cwd, offset: 0, limit: limit, errorBag: bag)
         case .opencode:
-            return loadOpenCodeEntries(needle: "", cwdFilter: cwd, offset: 0, limit: limit, errorBag: bag)
+            results = loadOpenCodeEntries(needle: "", cwdFilter: cwd, offset: 0, limit: limit, errorBag: bag)
         }
+        `#if` DEBUG
+        let errs = bag.snapshot()
+        if !errs.isEmpty {
+            dlog("session.latestEntries agent=\(agent.rawValue) errors=\(errs.count) first=\(errs.first ?? "")")
+        }
+        `#endif`
+        return results
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SessionIndexStore.swift` around lines 522 - 534, latestEntries
currently creates an ErrorBag (bag) and passes it to
loadCodexEntries/loadOpenCodeEntries but then discards any errors, causing
schema/open failures to be silent; update latestEntries to, under `#if` DEBUG, log
bag.snapshot() (or otherwise dlog the snapshot) after calling loadCodexEntries
and loadOpenCodeEntries so schema/open errors are visible during debugging while
preserving current return behavior; refer to the symbols latestEntries,
ErrorBag, bag.snapshot(), loadCodexEntries, loadOpenCodeEntries, and dlog/#if
DEBUG when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/SessionIndexStore.swift`:
- Around line 522-534: latestEntries currently creates an ErrorBag (bag) and
passes it to loadCodexEntries/loadOpenCodeEntries but then discards any errors,
causing schema/open failures to be silent; update latestEntries to, under `#if`
DEBUG, log bag.snapshot() (or otherwise dlog the snapshot) after calling
loadCodexEntries and loadOpenCodeEntries so schema/open errors are visible
during debugging while preserving current return behavior; refer to the symbols
latestEntries, ErrorBag, bag.snapshot(), loadCodexEntries, loadOpenCodeEntries,
and dlog/#if DEBUG when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f4b723d-34fd-46c2-b648-0bf40465ddcb

📥 Commits

Reviewing files that changed from the base of the PR and between 9e0a4be and 78fea9d.

📒 Files selected for processing (4)
  • Sources/SessionIndexStore.swift
  • Sources/SessionPersistence.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/TerminalController.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/SessionPersistence.swift
  • Sources/Workspace.swift

@yourconscience
Copy link
Copy Markdown
Author

Closing - duplicates #2978 which covers the same agent session restore feature with a more comprehensive approach (menu item, shortcut, CLI). Happy to see it land from the maintainer side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant